package asteroids.model;

import java.util.*;
import be.kuleuven.cs.som.annotate.*;
import asteroids.CollisionListener;
import asteroids.Util;

/**
 * A class representing a world.
 * @invar The dimensions of this world are valid.
 * 			| Util.isValidDouble(width) && Util.isValidDouble height) && 
 * 			| Util.fuzzyBetweenOrEqualTo(width, 0, maxSize) && 
 * 			| Util.fuzzyBetweenOrEqualTo(height, 0, maxSize)
 * @invar Every space object in this world is known.
 * 			| for each SpaceObject from getSpaceObjects()
 * 			| 	containsSpaceObject(object)
 * @invar Every space object is within the world's borders at all time.
 * 			| for each SpaceObject from getSpaceObjects()
 * 			| 	isWithinBorders(object)
 * @author Martin & Anton
 *
 */
public class World {

	private static final double maxSize = Double.MAX_VALUE;
	private final double width;
	private final double height;
	
	private Set<SpaceObject> spaceObjects;
	private Set<Collision> collisions;
	
	/**
	 * Constructor of a new World.
	 * @param width
	 * @param height
	 * @throws IllegalArgumentException
	 * 			| if(! (Util.isValidDouble(width) && Util.isValidDouble(height)
	 * 			| 		&& Util.fuzzyBetweenOrEqualTo(width,0,maxSize) 
	 * 			|		&& Util.fuzzyBetweenOrEqualTo(height,0,maxSize)))
	 */
	public World(double width, double height) throws IllegalArgumentException{
		if(Util.isValidDouble(width) && Util.isValidDouble(height) 
			&& Util.fuzzyBetweenOrEqualTo(width, 0, maxSize) && Util.fuzzyBetweenOrEqualTo(height, 0, maxSize)){
				this.width = width;
				this.height = height;
				spaceObjects = new HashSet<SpaceObject>();
				collisions = new HashSet<Collision>();
		}
		else{
			throw new IllegalArgumentException();
		}
	}
	
	@Basic @Immutable
	public double getWidth(){
		return width;
	}
	
	@Basic @Immutable
	public double getHeight(){
		return height;
	}
	
	/**
	 * Returns a Set of Ship-objects.
	 * @return	ships
	 * 		| for each SpaceObject from getSpaceObject{
	 * 		|	if(Ship.class.isInstance(object))
	 * 		|		ships.contains(object) == true
	 * 		|	else
	 * 		|		ships.contains(object) == false
	 * 		| }
	 */
	public Set<Ship> getShips(){
		Iterator<SpaceObject> i = spaceObjects.iterator();
		Set<Ship> ships = new HashSet<Ship>();
		while(i.hasNext()){
			SpaceObject next = i.next();
			if(Ship.class.isInstance(next))
				ships.add((Ship) next);
		}
		return ships;
	}

	/**
	 * Returns a Set of Asteroid-objects.
	 * @return	asteroids
	 * 		| for each SpaceObject from getSpaceObject{
	 * 		|	if(Asteroid.class.isInstance(object))
	 * 		|		asteroids.contains(object) == true
	 * 		|	else
	 * 		|		asteroids.contains(object) == false
	 * 		| }
	 */
	public Set<Asteroid> getAsteroids(){
		Iterator<SpaceObject> i = spaceObjects.iterator();
		Set<Asteroid> asteroids = new HashSet<Asteroid>();
		while(i.hasNext()){
			SpaceObject next = i.next();
			if(Asteroid.class.isInstance(next))
				asteroids.add((Asteroid) next);
		}
		return asteroids;
	}
	
	/**
	 * Returns a Set of Bullet-objects.
	 * @return	bullets
	 * 		| for each SpaceObject from getSpaceObject{
	 * 		|	if(Bullet.class.isInstance(object))
	 * 		|		bullets.contains(object) == true
	 * 		|	else
	 * 		|		bullets.contains(object) == false
	 * 		| }
	 */
	public Set<Bullet> getBullets(){
		Iterator<SpaceObject> i = spaceObjects.iterator();
		Set<Bullet> bullets = new HashSet<Bullet>();
		while(i.hasNext()){
			SpaceObject next = i.next();
			if(Bullet.class.isInstance(next))
				bullets.add((Bullet) next);
		}
		return bullets;
	}
	
	@Basic
	public Set<SpaceObject> getSpaceObjects(){
		return spaceObjects;
	}
	
	@Basic
	public Set<Collision> getCollisions(){
		return collisions;
	}
	
	/**
	 * Add a collision to the current set of collisions.
	 * @param collision
	 * @post If the given collision is not null, the collision will be in the set.
	 * 		| if(collision != null)
	 * 		|	(new this).getCollisions().contains(collision)
	 */
	public void addCollision(Collision collision){
		if(collision !=null)
			collisions.add(collision);
	}
	
	/**
	 * Add a set of collisions to the current set of collisions.
	 * @param extraCollisions
	 * @effect Every collision will be added.
	 * 		| for each Collision from extraCollisions
	 * 		|	addCollision(c)
	 */
	public void addCollisions(Set<Collision> extraCollisions){
		if(extraCollisions != null)
			for(Collision c : extraCollisions)
				addCollision(c);
	}
	
	/**
	 * Remove a collision from the current set of collisions.
	 * @param collision
	 * @post The collision will not be in the set.
	 * 		| ! collisions.contains(collision)
	 */
	public void removeCollision(Collision collision){
		collisions.remove(collision);
	}
	
	/**
	 * Gives a boolean that will say if the world contains this given spaceObject.
	 * @param object
	 * @return	false
	 * 			If the object is null or if it does not contain the Spaceobject.
	 * 			| if(object == null || !spaceObjects.contains(object))
	 * 			| 	Then return == false
	 * @return	true
	 * 			If the world contains the object.
	 * 			| if(spaceObjects.contains(object))
	 * 			|	Then return == true
	 */
	public boolean containsSpaceObject(SpaceObject object) {
		if(object == null)
			return false;
		if(!spaceObjects.contains(object))
			return false;
		return true;
	}
	
	/**
	 * This method adds a new spaceObject to this world.
	 * @param object
	 * @effect If the object is perfectly valid.
	 * 			| spaceObjects.add(object);
				| object.setWorld(this);
				| addCollisionsWith(object)
	 * @effect If the object is a bullet and it collides with another object.
	 * 			| collideSpaceObjects(object,collidingObject)
	 * @throws NullPointerException
	 * 			| if(object == null)
	 * @throws IllegalStateException
	 * 			| if(spaceObjects.contains(object))
	 * @throws IllegalArgumentException
	 * 			| if(!isWithinBorders(object))
	 * @throws IllegalArgumentException
	 * 			| if(object.overlapsWith(existingObject) && ! Bullet.class.isInstance(object))
	 */
	public void addSpaceObject(SpaceObject object) throws NullPointerException, IllegalArgumentException, IllegalStateException{
		if(object == null)
			throw new NullPointerException();
		if(spaceObjects.contains(object))
			throw new IllegalStateException();
		
		//check if the object lies fully within the world's borders.
		if(!isWithinBorders(object))
			throw new IllegalArgumentException();
		
		boolean resolved = false;
		//check if the object would overlap with an existing object
		Iterator<SpaceObject> i = spaceObjects.iterator();
		while(i.hasNext()){
			SpaceObject next = i.next();
			if(next.overlapsWith(object)){
				//If the new object is a bullet, it immediately collides with the overlapping entity,
				//except if the entity is it's mothership.
				if(Bullet.class.isInstance(object)){
					if(((Bullet) object).getShip() != next){
						spaceObjects.add(object);
						object.setWorld(this);
						collideSpaceObjects(object,next);
						resolved = true;
						break;
					}
				}
				//If the new object is a ship, and the overlapping object is it's bullet (for some reason)
				//then the overlapping is allowed.
				else if(Ship.class.isInstance(object) && Bullet.class.isInstance(next)){
					if(((Bullet) next).getShip() != object)
						throw new IllegalArgumentException();
				}
				//if the 2 objects are touching each other, but go in opposite directions (for example 2 child-asteroids)
				else if(!(Util.fuzzyEquals(object.getXVelocity(), -next.getXVelocity()) && 
						Util.fuzzyEquals(object.getYVelocity(), -next.getYVelocity()) && 
							Util.fuzzyEquals(next.getDistanceTo(object),0))){
					throw new IllegalArgumentException();
				}
			}
		}
		// If no Exception has been thrown, and there was no collision,
		// we can safely add the object and set the world of the ship to this world.
		if(!resolved){
			spaceObjects.add(object);
			object.setWorld(this);
			addCollisionsWith(object);
		}
	}
	
	/**
	 * Method to remove a space object from this world.
	 * @param object
	 * @effect
	 * 			| object.die()
	 * 			| removeCollisionsWithObjectsFrom(new Collision(object, null, 0))
	 * 			| spaceObjects.remove(object)
	 * @throws NullPointerException
	 * 			| if(object == null)
	 * @throws IllegalArgumentException
	 * 			| if(!spaceObjects.contains(object))
	 */
	public void removeSpaceObject(SpaceObject object) throws NullPointerException, IllegalArgumentException{
		if(object == null)
			throw new NullPointerException();
		if(spaceObjects.contains(object)) {
			removeCollisionsWith(object);
			spaceObjects.remove(object);
			object.die();
		}
		else{
			throw new IllegalArgumentException();
		}
	}
	
	/**
	 * Check if an object lies completely within this world's borders.
	 * @param object
	 * @return true, only if the space object lies within this world's borders.
	 * 			| if(Util.fuzzyBetweenOrEqualTo(x, 0+radius, getWidth()-radius)
				|	&& Util.fuzzyBetweenOrEqualTo(y, 0+radius, getHeight()-radius))
	 */
	public boolean isWithinBorders(SpaceObject object){
		double x = object.getX();
		double y = object.getY();
		double radius = object.getRadius();
		if(Util.fuzzyBetweenOrEqualTo(x, 0+radius, getWidth()-radius)
				&& Util.fuzzyBetweenOrEqualTo(y, 0+radius, getHeight()-radius))
			return true;
		else
			return false;
	}
	
	/**
	 * This method lets 2 space objects collide with each other.
	 * If one is a ship and the other also is a ship, they will bounce of of each other.
	 * If one is an asteroid and the other as well, they will bounce of each other.
	 * If one is a bullet, then it will destroy both of the objects.
	 * If one is an asteroid and the other is a ship, the ship will be destroyed, and nothing will happen to the asteroid.
	 * @param object1
	 * @param object2
	 * @effect  If the first object is a bullet, and the ship who fired the bullet is not the second object, or vica versa.
	 * 			| if(object1 instanceof Bullet && !(object2 instanceof Ship && ((Bullet) object1).getShip() == object2)) ||
	 * 			|	if(object2 instanceof Bullet && !(object1 instanceof Ship && ((Bullet) object2).getShip() == object1))
	 * 			| 	removeSpaceObject(object1) && removeSpaceObject(object2)
	 * @effect 	If the objects are both ships or are both asteroids.
	 * 			| if((object1 instanceof Asteroid && object2 instanceof Asteroid) ||
	 *			|		(object1 instanceof Ship 	 && object2 instanceof Ship))
	 *			|	object1.bounce(object2)
	 * @effect	If the second object is an asteroid and the first is a ship.
	 * 			| if(object1 instanceof Ship && object2 instanceof Asteroid)
	 * 			|	removeSpaceObject(object1)
	 * @effect	If the first object is an asteroid and the second a ship.
	 * 			| if(object2 instanceof Ship && object1 instanceof Asteroid)
	 * 			| 	removeSpaceObject(object2)
	 * @throws NullPointerException
	 * 			If one of the objects is null
	 * 			 | if(object1 == null || object2 == null)
	 */
	public void collideSpaceObjects(SpaceObject object1, SpaceObject object2) throws NullPointerException{
		if((object1 == null) || (object2 == null))
			throw new NullPointerException();
		if(object1 instanceof Bullet){ //Bullet vs. not his Ship
			if(!(object2 instanceof Ship && ((Bullet) object1).getShip() == object2)){
				removeSpaceObject(object1);
				removeSpaceObject(object2);
			}
		}
		else if(object2 instanceof Bullet){ //Ship vs. not his Bullet
			if(!(object1 instanceof Ship && ((Bullet) object2).getShip() == object1)){
				removeSpaceObject(object2);
				removeSpaceObject(object1);
			}
		}
		//Asteroid vs. Asteroid or Ship vs. Ship?
		else if((object1 instanceof Asteroid && object2 instanceof Asteroid) ||
				(object1 instanceof Ship 	 && object2 instanceof Ship))
			object1.bounce(object2);
		
		else{ //Ship vs. Asteroid
			if(object1 instanceof Ship) 
				removeSpaceObject(object1);
			else if(object2 instanceof Ship) 
				removeSpaceObject(object2);
		}		
	}
	
	/**
	 * This method will collide a space object with a boundary. 
	 * @param object
	 * @effect object.bounceWall()
	 * @throws NullPointerException
	 * 			If the object is null.
	 * 			| if(object == null)
	 */
	public void collideBoundary(SpaceObject object) throws NullPointerException{
		object.bounceWall();
	}
	
	/**
	 * This method will advance time, until a collision happens.
	 * It will resolve the collision, move all objects, accelerate all thrusting ships
	 * and repeat until the given time has been reached.
	 * @param time
	 * @param collisionListener
	 * @variable collisions
	 * 			| A list containing all collisions from every spaceObject in this world.
	 * 			| ArrayList collisions = new ArrayList<Collision>()
	 * 			| for each object from getSpaceObjects()
	 * 			|	collisions.containsAll(addCollisionsWith(object))
	 * @variable firstCollision
	 * 			| The first collision that will happen.
	 * 			| firstCollision == getFirstCollision(collisions)
	 * @effect If no collision happens in the given time, every ship will move during the specified time, and
	 * 			| every Ship's velocity will be updated regarding its thrust.
	 * 			| for each object from this.spaceObjects : 
	 * 			| 	object.move(time)
	 * 			| for each ship from this.getShips() : 
	 * 			| 	ship.thrust(time)
	 * @effect 	After every collision or the given time, every ship will move during the specified time.
	 * 			| while(time>0){
	 * 			| 	for each object from this.spaceObjects : 
	 * 			| 		object.move(firstCollision.getCollisionTime())
	 * 			|	time = time - firstCollision.getCollisionTime()
	 * 			| }
	 * @effect	After every collision or time, the speed of each ship will be recalculated in relation to its thruster.
	 * 			| while(time>0){
	 * 			|	for each Ship from getShips() :
	 * 			|		ship.thrust(firstCollision.getCollisionTime())
	 * 			| }
	 * @effect	If any collision with 2 objects happens, this collision will be resolved.
	 * 			| while(time>0){
	 * 			| 	collideSpaceObjects(firstCollision.getFirstObject(), firstCollision.getSecondObject())
	 * 			| 	collisionListener.objectCollision(firstCollision.getFirstObject(), firstCollision.getSecondObject(), 
	 * 			|						firstCollision.getX(), firstCollision.getY())
	 * 			|	time = time - firstCollision.getCollisionTime()
	 * 			| }
	 * @effect	If any collision between an object and one of the boundaries happens, this collision will be resolved.
	 * 			| while(time>0){
	 * 			| 	collideBoundary(firstCollision.getFirstObject())
	 * 			| 	collisionListener.boundaryCollision(firstCollision.getFirstObject(), 
	 * 			|						firstCollision.getX(), firstCollision.getY())
	 * 			| 	time = time - firstCollision.getCollisionTime()
	 * 			| }
	 * @throws IllegalArgumentException
	 * 			| If time is not valid or smaller than 0.
	 * 			| !Util.isValidDouble(time) || time<0
	 */
	public void evolve(double time, CollisionListener collisionListener) throws IllegalArgumentException{
		// First check if it is a valid time
		if(!Util.isValidDouble(time) || Util.fuzzyLessThan(time, 0))
			throw new IllegalArgumentException();
		if(Util.fuzzyLessThanOrEqualTo(time, 0))
			time = 0;
		
		// We iterate until time reaches 0.
		while((time>0) && !collisions.isEmpty()) {
			Collision firstCollision = getFirstCollision();
			double firstCollisionTime = firstCollision.getTimeToCollision();
			// If the returned time is slightly lower than 0, then we set the time to 0.
			if(Util.fuzzyLessThanOrEqualTo(firstCollisionTime,0))
				firstCollisionTime = 0;
			// If the first collision happens in the time we have left, then we can already 
			// move every object for that time.
			if(firstCollisionTime < time) {
				for(SpaceObject object: spaceObjects)
					object.move(firstCollisionTime);
				substractCollisionTime(firstCollisionTime);
				//We resolve the collision
				if(firstCollision.getSecondObject() == null){
					collideBoundary(firstCollision.getFirstObject());
					collisionListener.boundaryCollision(firstCollision.getFirstObject(), firstCollision.getX(), firstCollision.getY());
				}
				else{
					collideSpaceObjects(firstCollision.getFirstObject(), firstCollision.getSecondObject());
					collisionListener.objectCollision(firstCollision.getFirstObject(), firstCollision.getSecondObject(), firstCollision.getX(), firstCollision.getY());
				}
				removeCollision(firstCollision);
				
				// We remove all collisions that were going to happen with the two collided objects, 
				// because it is likely that they exploded, or that their velocity has changed.
				removeCollisionsWithObjectsFrom(firstCollision);
				
				// We also let the ships with a thruster thrust.
				// We remove all the collisions the ships with an active thruster would have, 
				// and let them recalculate their new collisions, depending of the new velocity.
				// This happens after the collision because it would create anomalies if a thrusting ship 
				// was the first colliding ship or would be destroyed.
				for(Ship ship: getShips()){
					if(ship.isThrusterActive()) {
						ship.thrust(firstCollisionTime);
						removeCollisionsWith(ship);
						addCollisionsWith(ship);
					}
				}
				//We check for new collisions, with the 2 colliding objects (if they still exist), and with
				//new objects (if new 'child-asteroids' were born).
				List<SpaceObject> objectsToCheck = new ArrayList<SpaceObject>();
				if(containsSpaceObject(firstCollision.getFirstObject()))
					objectsToCheck.add(firstCollision.getFirstObject());
				if(firstCollision.getSecondObject() != null && containsSpaceObject(firstCollision.getSecondObject()))
					objectsToCheck.add(firstCollision.getSecondObject());
				//we add the new collisions
				for(SpaceObject newObject: objectsToCheck){
					addCollisionsWith(newObject);
				}
				for(Ship ship : getShips()){
					ship.execute(firstCollisionTime);
				}
				time = time - firstCollisionTime;
				
			}
			// If the first collision happens after the time left, we move for the left over time and also thrust, and set time to 0;
			else{
				for(SpaceObject object : spaceObjects)
					object.move(time);
				for(Ship ship: getShips()){
					if(ship.isThrusterActive()){
						ship.thrust(time);
						removeCollisionsWith(ship);
						addCollisionsWith(ship);
					}
					ship.execute(time);
				}
				substractCollisionTime(time);
				time = 0;
			}
		}
		if(getCollisions().isEmpty()){
			for(SpaceObject object : spaceObjects)
				object.move(time);
			for(Ship ship: getShips()){
				if(ship.isThrusterActive()) {
					ship.thrust(time);
					removeCollisionsWith(ship);
					addCollisionsWith(ship);
				}
				ship.execute(time);
			}
			substractCollisionTime(time);
		}
		
	}
	
	/**
	 * Get the first collision of a list of collisions.
	 * @param collisions
	 * @return firstCollision
	 * @result No collision will happen before the returned collision.
	 * 			| for(Collision collision: collisions){
	 * 			| 	!Util.fuzzyLessThan(collision.getTimeToCollision(),firstCollision.getTimeToCollision()))
	 */
	public Collision getFirstCollision() {
		double firstCollisionTime = Double.POSITIVE_INFINITY;
		Collision firstCollision = null;
		// We get the first collision.
		for(Collision collision: getCollisions()) {
			if(collision.getTimeToCollision() < firstCollisionTime){
				firstCollisionTime = collision.getTimeToCollision();
				firstCollision = collision;
			}
		}
		return firstCollision;
	}
	
	/**
	 * Remove collisions from a given list of collisions, if one of their objects appears in a given collision.
	 * @param collisions
	 * @param collision
	 * @return collisions
	 * @result In the returned list, no collision has objects that appear in the given collision.
	 * 			| for each Collision from getCollisions
	 * 			|  ! collision.containsAnObjectFrom(collision)
	 * 
	 */
	public void removeCollisionsWith(SpaceObject object) {
		Iterator<Collision> i = getCollisions().iterator();
		while (i.hasNext()) {
			Collision nextCollision = i.next();
			if(nextCollision.contains(object))
				i.remove();
		}
	}
	
	/**
	 * Remove collisions from a given list of collisions, if one of their objects appears in a given collision.
	 * @param collisions
	 * @param collision
	 * @return collisions
	 * @result In the returned list, no collision has objects that appear in the given collision.
	 * 			| for each Collision from getCollisions
	 * 			|  ! collision.containsAnObjectFrom(collision)
	 * 
	 */
	public void removeCollisionsWithObjectsFrom(Collision collision) {
		Iterator<Collision> i = getCollisions().iterator();
		while (i.hasNext()) {
			Collision nextCollision = i.next();
			if(nextCollision.containsAnObjectFrom(collision))
				i.remove();
		}
	}
	
	/**
	 * Substract a given time from every collision in a given list of collisions.
	 * @param collisions
	 * @param time
	 * @effect 
	 * 			| for each Collision from getCollisions
	 * 			|	collision.substractTime(time)
	 * @throws IllegalArgumentException
	 */
	public void substractCollisionTime(double time) throws IllegalArgumentException{
		if(!Util.isValidDouble(time) || Util.fuzzyLessThan(time, 0))
			throw new IllegalArgumentException("time was" + time);
		for(Collision collision: collisions){
			collision.substractTime(time);
		}
	}
	
	/**
	 * Returns a list of collisions that will happen to a given object.
	 * @param collider
	 * @post The list contains all collisions with other objects.
	 * 			| for each SpaceObject from getSpaceObjects()
	 * 			| 	if(!Double.isInfinite(collider.getTimeToCollision(object)) && !Util.fuzzyLessThan(collider.getTimeToCollision(object), 0))
	 * 			| 		collisions.add(new Collision(collider, object, collider.getTimeToCollision(object)))
	 * @post The list contains the next boundary collision.
	 * 			| if(!Double.isInfinite(collider.getTimeToBoundaryCollision()) && !Util.fuzzyLessThan(collider.getTimeToBoundaryCollision(), 0))
	 *			|	collisions.add(new Collision(collider, null, collider.getTimeToBoundaryCollision()))
	 * @post The list does not contain collisions between a bullet and it's mothership.
	 * 			| for each Collision from getCollisions{
	 * 			| 	if(collision.getFirstObject() instanceof Bullet)
	 * 			|		collision.getSecondObject() != ((Bullet) collision.getFirstObject()).getShip()
	 * 			| 	if(collision.getSecondObject() instanceof Bullet)
	 * 			|		collision.getFirstObject() != ((Bullet) collision.getSecondObject()).getShip()
	 * 			| }
	 */
	public void addCollisionsWith(SpaceObject collider) {
		// A temporary Set for new collisions.
		Set<Collision> extraCollisions = new HashSet<Collision>();
		if(collider == null)
			return;
		for(SpaceObject object: spaceObjects) {
			if(!Double.isInfinite(collider.getTimeToCollision(object)) && !Util.fuzzyLessThan(collider.getTimeToCollision(object), 0))
				extraCollisions.add(new Collision(collider, object, collider.getTimeToCollision(object)));
		}
		if(!Double.isInfinite(collider.getTimeToBoundaryCollision()) && !Util.fuzzyLessThan(collider.getTimeToBoundaryCollision(), 0))
			extraCollisions.add(new Collision(collider, null, collider.getTimeToBoundaryCollision()));
		
		//remove collisions between a bullet and it's mother ship
		if(collider instanceof Bullet){
			Iterator<Collision> i = extraCollisions.iterator();
			while (i.hasNext()) {
				Collision nextCollision = i.next();
				if((nextCollision.getFirstObject() == ((Bullet) collider).getShip()) || 
					(nextCollision.getSecondObject() == ((Bullet) collider).getShip()))
					i.remove();
			}
		}
		else if(collider instanceof Ship){
			Iterator<Collision> i = extraCollisions.iterator();
			while (i.hasNext()) {
				Collision nextCollision = i.next();
				if((nextCollision.getFirstObject() instanceof Bullet) && 
					((Bullet) nextCollision.getFirstObject()).getShip() == collider)
					i.remove();
				else if((nextCollision.getSecondObject() instanceof Bullet) && 
						((Bullet) nextCollision.getSecondObject()).getShip() == collider)
					i.remove();
			}
		}
		addCollisions(extraCollisions);
	}
	
	/**
	 * For sake of testing, we make a method without the collisionListner.
	 * @param time
	 * @throws IllegalArgumentException
	 * @effect evolve(time, collisionListener)
	 */
	public void evolve(double time) throws IllegalArgumentException{
		// First check if it is a valid time
		if(!Util.isValidDouble(time) || Util.fuzzyLessThan(time, 0))
			throw new IllegalArgumentException();
		if(Util.fuzzyEquals(time, 0))
			time = 0;
		
		// We iterate until time reaches 0.
		while((time>0) && !collisions.isEmpty()) {
			Collision firstCollision = getFirstCollision();
			double firstCollisionTime = firstCollision.getTimeToCollision();
			// If the returned time is slightly lower than 0, then we set the time to 0.
			if(Util.fuzzyEquals(firstCollisionTime,0))
				firstCollisionTime = 0;
			// If the first collision happens in the time we have left, then we can already 
			// move every object for that time.
			if(firstCollisionTime < time) {
				for(SpaceObject object: spaceObjects)
					object.move(firstCollisionTime);
				substractCollisionTime(firstCollisionTime);
				//We resolve the collision
				if(firstCollision.getSecondObject() == null){
					collideBoundary(firstCollision.getFirstObject());
				}
				else{
					collideSpaceObjects(firstCollision.getFirstObject(), firstCollision.getSecondObject());
				}
				removeCollision(firstCollision);
				
				// We remove all collisions that were going to happen with the two collided objects, 
				// because it is likely that they exploded, or that their velocity has changed.
				removeCollisionsWithObjectsFrom(firstCollision);
				
				// We also let the ships with a thruster thrust.
				// We remove all the collisions the ships with an active thruster would have, 
				// and let them recalculate their new collisions, depending of the new velocity.
				// This happens after the collision because it would create anomalies if a thrusting ship 
				// was the first colliding ship or would be destroyed.
				for(Ship ship: getShips()){
					if(ship.isThrusterActive()) {
						ship.thrust(firstCollisionTime);
						removeCollisionsWith(ship);
						addCollisionsWith(ship);
					}
				}
				//We check for new collisions, with the 2 colliding objects (if they still exist), and with
				//new objects (if new 'child-asteroids' were born).
				List<SpaceObject> objectsToCheck = new ArrayList<SpaceObject>();
				if(containsSpaceObject(firstCollision.getFirstObject()))
					objectsToCheck.add(firstCollision.getFirstObject());
				if(firstCollision.getSecondObject() != null && containsSpaceObject(firstCollision.getSecondObject()))
					objectsToCheck.add(firstCollision.getSecondObject());
				//we add the new collisions
				for(SpaceObject newObject: objectsToCheck){
					addCollisionsWith(newObject);
				}
				for(Ship ship : getShips()){
					ship.execute(firstCollisionTime);
				}
				time = time - firstCollisionTime;
				
			}
			// If the first collision happens after the time left, we move for the left over time and also thrust, and set time to 0;
			else{
				for(SpaceObject object : spaceObjects)
					object.move(time);
				for(Ship ship: getShips()){
					if(ship.isThrusterActive()){
						ship.thrust(time);
						removeCollisionsWith(ship);
						addCollisionsWith(ship);
					}
					ship.execute(time);
				}
				substractCollisionTime(time);
				time = 0;
			}
		}
		if(getCollisions().isEmpty()){
			for(SpaceObject object : spaceObjects)
				object.move(time);
			for(Ship ship: getShips()){
				if(ship.isThrusterActive()) {
					ship.thrust(time);
					removeCollisionsWith(ship);
					addCollisionsWith(ship);
				}
				ship.execute(time);
			}
			substractCollisionTime(time);
		}
		
	}
}
